package com.googlecode.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.InflaterInputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public enum CompressionMode {
	/**
	 * Gzip mode
	 */
	GZIP,
	/**
	 * Zip mode
	 */
	ZIP;
	private static final Logger logger = LoggerFactory.getLogger(CompressionMode.class);

	public byte[] compress(byte[] in) {
		if (in == null || in.length == 0)
			throw new IllegalArgumentException("compress data must not be blank");

		switch (this) {
		case GZIP:
			return gzipCompress(in);
		case ZIP:
			return zipCompress(in);
		default:
			throw new IllegalArgumentException("[GZIP | ZIP]");
		}
	}

	public byte[] uncompress(byte[] in) {
		if (in == null || in.length == 0)
			throw new IllegalArgumentException("uncompress data must not be blank");

		switch (this) {
		case GZIP:
			return gzipUncompress(in);
		case ZIP:
			return zipUncompress(in);
		default:
			throw new IllegalArgumentException("[GZIP | ZIP]");
		}
	}

	private final static byte[] gzipCompress(final byte[] in) {
		assert (in != null);
		
		GZIPOutputStream gz = null;
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		try {
			gz = new GZIPOutputStream(bos);
			gz.write(in);
		} catch (IOException e) {
			throw new RuntimeException("IO exception compressing data", e);
		} finally {
			if (gz != null) {
				try {
					gz.close();
				} catch (IOException e) {
					logger.error("Failed to Close GZIPOutputStream", e);
				}
			}
			try {
				bos.close();
			} catch (IOException e) {
				logger.error("Failed to Close ByteArrayOutputStream", e);
			}
		}
		byte[] rv = bos.toByteArray();
		if (logger.isDebugEnabled()) {
			logger.debug(String.format("gzipCompressed %d bytes to %d", in.length, rv.length));
		}
		return rv;
	}

	private final static byte[] gzipUncompress(final byte[] in) {
		assert (in != null);
		
		GZIPInputStream gis = null;
		ByteArrayOutputStream bos = null;
		ByteArrayInputStream bis = new ByteArrayInputStream(in);
		try {
			gis = new GZIPInputStream(bis);
			bos = new ByteArrayOutputStream();
			int len;
			byte[] outbuf = new byte[in.length];
			while ((len = gis.read(outbuf, 0, outbuf.length)) != -1) {
				bos.write(outbuf, 0, len);
			}
			bos.flush();
		} catch (IOException e) {
			logger.error("Failed to Uncompress data", e);
			bos = null;
		} finally {
			if (gis != null) {
				try {
					gis.close();
				} catch (IOException e) {
					logger.error("Failed to Close GZIPInputStream", e);
				}
			}
			try {
				bis.close();
			} catch (IOException e) {
				logger.error("Failed to Close ByteArrayInputStream", e);
			}
			if(bos != null) {
				try {
					bos.close();
				} catch (IOException e) {
					logger.error("Failed to Close ByteArrayOutputStream", e);
				}
			}
		}
		return (bos == null) ? null : bos.toByteArray();
	}

	private final static byte[] zipCompress(final byte[] in) {
		assert (in != null);
		ByteArrayOutputStream bos = new ByteArrayOutputStream(in.length);
		DeflaterOutputStream os = new DeflaterOutputStream(bos);
		try {
			os.write(in);
		} catch (IOException e) {
			throw new RuntimeException("IO exception compressing data", e);
		} finally {
			try {
				os.close();
			} catch (IOException e) {
				logger.error("Failed to Close DeflaterOutputStream", e);
			}
			try {
				bos.close();
			} catch (IOException e) {
				logger.error("Failed to Close ByteArrayOutputStream", e);
			}
		}
		return bos.toByteArray();
	}

	private static final int COMPRESS_RATIO = 8;

	private final static byte[] zipUncompress(final byte[] in) {
		assert (in != null);
		int size = in.length * COMPRESS_RATIO;
		ByteArrayInputStream bis = new ByteArrayInputStream(in);
		InflaterInputStream is = new InflaterInputStream(bis);
		ByteArrayOutputStream bos = new ByteArrayOutputStream(size);
		try {
			byte[] uncompressMessage = new byte[size];
			int len;
			while ((len = is.read(uncompressMessage)) > 0) {
				bos.write(uncompressMessage, 0, len);
			}
			bos.flush();
		} catch (IOException e) {
			logger.error("Failed to Uncompress data", e);
			bos = null;
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				logger.error("Failed to Close InflaterInputStream");
			}
			try {
				bis.close();
			} catch (IOException e) {
				logger.error("Failed to Close ByteArrayInputStream");
			}
			try {
				bos.close();
			} catch (IOException e) {
				logger.error("Failed to Close ByteArrayOutputStream");
			}
		}
		return (bos == null) ? null : bos.toByteArray();
	}
}
